import { assign, hasIntersectionObserver, isObject, loadImage } from "@/utils";
import { CallbackFunction, LazyOptions, ValueFormatterObject } from "./types";
import { CssStyleObject } from "@/utils/types";

enum LifecycleEnum {
  LOADING = "loading",
  LOADED = "loaded",
  ERROR = "error",
}

const DEFAULT_OBSERVER_OPTIONS = {
  rootMargin: "0px",
  threshold: 0,
};

const DEFAULT_LOADING =
  "";
const DEFAULT_ERROR = "";

export default class Lazy {
  lazyActive = true;
  crossOrigin = true; // 开启跨域
  options: LazyOptions = {
    loading: DEFAULT_LOADING,
    error: DEFAULT_ERROR,
    observerOptions: DEFAULT_OBSERVER_OPTIONS,
    log: true,
    ratioCalculator: (width: number, height: number) => height / width,
  };

  _images = new WeakMap();

  constructor(flag = true, options: LazyOptions, crossOrigin = true) {
    this.lazyActive = flag;
    this.crossOrigin = crossOrigin;
    this.config(options);
  }

  config(options = {} as LazyOptions) {
    assign(this.options, options);
    options.ratioCalculator &&
      (this.options.ratioCalculator = options.ratioCalculator);
  }

  // mount
  mount(
    el: HTMLImageElement,
    binding: string | ValueFormatterObject,
    callback: CallbackFunction
  ): void {
    const { src, loading, error } = this._valueFormatter(binding);
    el.setAttribute("lazy", LifecycleEnum.LOADING);
    el.setAttribute("src", loading || DEFAULT_LOADING);
    if (!this.lazyActive) {
      this._setImageSrc(el, src, callback, error);
    } else {
      if (!hasIntersectionObserver) {
        this._setImageSrc(el, src, callback, error);
        this._log(() => {
          throw new Error("Not support IntersectionObserver!");
        });
      }
      this._initIntersectionObserver(el, src, callback, error);
    }
  }

  // resize
  resize(el: HTMLImageElement, callback: () => void) {
    const lazy = el.getAttribute("lazy");
    const src = el.getAttribute("src");
    if (lazy && lazy === LifecycleEnum.LOADED && src) {
      loadImage(src, this.crossOrigin).then((image: HTMLImageElement) => {
        const { width, height } = image;
        const curHeight = (el.width / width) * height;
        el.height = curHeight;
        const style = el.style as CssStyleObject;
        style.height = `${curHeight}px`;
        callback();
      });
    }
  }

  // unmount
  unmount(el: HTMLElement): void {
    const imgItem = this._realObserver(el);
    imgItem && imgItem.unobserve(el);
    this._images.delete(el);
  }

  /**
   * 设置img的src
   * @param {*} el - img
   * @param {*} src - 原图
   * @param {*} error - 错误图片
   * @param {*} callback - 完成的回调函数，通知组件刷新布局
   * @returns
   */
  _setImageSrc(
    el: HTMLImageElement,
    src: string,
    callback: CallbackFunction,
    error?: string
  ): void {
    if (!src) return;

    const preSrc = el.getAttribute("src");
    if (preSrc === src) return;

    loadImage(src, this.crossOrigin)
      .then((image: HTMLImageElement) => {
        // 修改容器
        const { width, height } = image;
        const ratio =
          this.options.ratioCalculator?.(width, height) || height / width;
        const lazyBox = el.parentNode!.parentNode as HTMLElement;
        lazyBox.style.paddingBottom = `${ratio * 100}%`;

        // 设置图片
        el.setAttribute("lazy", LifecycleEnum.LOADED);
        el.removeAttribute("src");
        el.setAttribute("src", src);
        callback(true);
      })
      .catch((error) => {
        const imgItem = this._realObserver(el);
        imgItem && imgItem.disconnect();
        if (error) {
          el.setAttribute("lazy", LifecycleEnum.ERROR);
          el.setAttribute("src", error);
          callback(false);
        }
        this._log(() => {
          throw new Error(`Image failed to load!And failed src was: ${src} `);
        });
      });
  }

  _isOpenLazy(): boolean {
    return hasIntersectionObserver && this.lazyActive;
  }

  /**
   * 添加img和对应的observer到weakMap中
   * 开启监听
   * 当出现在可视区域后取消监听
   * @param {*} el - img
   * @param {*} src - 图片
   * @param {*} error - 错误图片
   * @param {*} callback - 完成的回调函数，通知组件刷新布局
   */
  _initIntersectionObserver(
    el: HTMLImageElement,
    src: string,
    callback: CallbackFunction,
    error?: string
  ): void {
    const observerOptions = this.options.observerOptions;
    this._images.set(
      el,
      new IntersectionObserver((entries) => {
        Array.prototype.forEach.call(entries, (entry) => {
          if (entry.isIntersecting) {
            const imgItem = this._realObserver(el);
            imgItem && imgItem.unobserve(entry.target);
            this._setImageSrc(el, src, callback, error);
          }
        });
      }, observerOptions)
    );
    const imgItem = this._realObserver(el);
    imgItem && imgItem.observe(el);
  }

  // 格式化参数
  _valueFormatter(value: ValueFormatterObject | string): ValueFormatterObject {
    let src = value as string;
    let loading = this.options.loading;
    let error = this.options.error;
    if (isObject(value)) {
      src = (value as ValueFormatterObject).src;
      loading = (value as ValueFormatterObject).loading || this.options.loading;
      error = (value as ValueFormatterObject).error || this.options.error;
    }
    return {
      src,
      loading,
      error,
    };
  }

  // 日志
  _log(callback: () => void): void {
    if (this.options.log) callback();
  }

  // 在map中获取对应img的observer事件
  _realObserver(el: HTMLElement): IntersectionObserver | undefined {
    return this._images.get(el);
  }
}
